<!DOCTYPE html>
<!-- Copyright © 2021 by wzh -->
<html lang="zh">
	<head>
		<meta charset="UTF-8" />
		<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" />
		<title>三体运动模拟</title>
		
		<!--icon-->
		<link href="./img/icon.png" rel="icon" type="image/x-icon"/>
		
		<!-- 异步fonts加载 -->
		<script src="./js/webfontloader.js"></script>
		<script>
		if (typeof require != "undefined"){ //electron
			console.log("electron")
			document.write(`<link href="./css/fonts.css" rel="stylesheet" type="text/css" />`);
			window.onload = function(){
				console.log("onload")
				$("body").addClass("wf-active");
			}
		}else{
			WebFont.load({
				google: {
					families: ["kt", "st", "xs", "zyyt"],
					api: "./css/fonts.css"
				}
			});
		}
		</script>
		
		<style>
		/* layui */
		.layui-input[type="color"]{
			padding: 0 0.2rem;
		}
		.layui-btn-group .layui-btn:not(:first-child){
			border-left-style: none !important;
		}
		
		/* daovoice */
		.daodream-container.daodream-reset{
			position: absolute;
		}
		
		/* element */
		*{
			outline: none; /* 无点击线 */
		}
		body{
			margin: 0;
		}
		
		/* settings */
		.settings{
			position: absolute;
			width: 100%;
			min-height: 100%;
			background-color: white;
		}
		.settings.landscape{
			padding: 0 20vw;
			width: 60vw;
		}
		
		.settings > h1{
			text-align: center;
			font-size: 2em;
			font-weight: bold;
			margin: 1rem;
		}
		
		.settings > form .item > label{
			display: block;
		}
		
		.settings > form > .btns{
			display: flex;
			width: 100%;
		}
		.settings > form > .btns > button{
			width: 50%;
			margin: 0.3rem;
			font-size: 1.5em;
			font-weight: bold;
		}
		
		.settings > form > .controls{
			margin: 0.6rem;
		}
		.settings > form > .controls > i{
			padding: 0.6rem;
			font-size: 3em !important;
		}
		
		
		/* set */
		#set{
			position: absolute;
			right: 0;
			top: 0;
			z-index: 1;
			display: flex;
			flex-direction: column;
			color: white;
		}
		#set > i{
			display: block;
			font-size: 3.6em;
		}
		#set > label{
			display: block;
			margin: 0 auto;
			margin-top: -5px;
			font-size: 1.1em;
			font-weight: bold;
		}
		
		/* speed */
		#speed{
			display: block;
			position: absolute;
			top: 0;
			left: 50%;
			transform: translateX(-50%);
			z-index: 1;
			width: 50vw;
		}
		
		/* canvas */
		canvas{
			position: absolute;
			width: 100%;
			height: 100%;
		}
		</style>
		
		<!-- jquery.js -->
		<script src="./js/jquery.min.js"></script>
		
		<!-- vue.js -->
		<script src="./js/vue3.js"></script>
		
		<!-- three.js -->
		<script src="./js/three.min.js"></script>
		<script src="./js/OrbitControls.js"></script>
		<!-- stats.js -->
		<script src="./js/stats.min.js"></script>
		
		<!-- layui -->
		<!--<link rel="stylesheet" href="https://unpkg.com/layui@2.6.4/dist/css/layui.css">
		<script src="https://unpkg.com/layui@2.6.8/dist/layui.js"></script>-->
		<script src="./layui/layui.js"></script>
		<link rel="stylesheet" href="./layui/css/layui.css" />
		
		<!-- 调试工具 -->
		<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
		<script src="https://cdn.bootcss.com/vConsole/3.3.4/vconsole.min.js"></script>
		<script>
		if ( /ipad|iphone|midp|rv:1.2.3.4|ucweb|android|windows ce|windows mobile/.test( navigator.userAgent.toLowerCase() ) ){
			//手机
			eruda.init();
			new VConsole();
		}
		</script>
		
		<script src="./js/js_plus.min.js"></script> <!-- js+ -->
		
		<script src="https://wzh656.github.io/threebody-simulation/js/dynamic.js" async></script>
		<script src="https://wzh656.github.io/threebody-simulation/js/count.js" async></script>
		
	</head>
	<body>
		<app>
			<section :class="[{landscape: landscape}, {settings: true}]" v-show="setting">
				<h1>配置</h1>
				<form class="layui-form" novalidate @submit.prevent="submit">
					<div class="inputs layui-collapse">
						<div class="item layui-colla-item" v-for="(body, index) of bodies">
							<label class="layui-colla-title">Body {{index+1}}</label>
							<div class="layui-colla-content">
								
								<div class="layui-form-item" v-for="(value, name) of body.pos">
									<label class="layui-form-label">{{name}}坐标</label>
									<div class="layui-input-inline">
										<input type="number" v-model="body.pos[name]" class="pos layui-input" />
									</div>
								</div>
								
								<div class="layui-form-item" v-for="(value, name) of body.v">
									<label class="layui-form-label">{{name}}分速度</label>
									<div class="layui-input-inline">
										<input type="number" v-model="body.v[name]" class="v layui-input" />
									</div>
								</div>
								
								<div class="layui-form-item">
									<label class="layui-form-label">颜色</label>
									<div class="layui-input-inline">
										<input type="color" v-model="body.color" class="color layui-input" />
									</div>
								</div>
								
								<div class="layui-form-item">
									<label class="layui-form-label">质量</label>
									<div class="layui-input-inline">
										<input type="number" v-model="body.m" class="mass layui-input" />
									</div>
								</div>
								
								<div class="layui-form-item">
									<label class="layui-form-label">半径</label>
									<div class="layui-input-inline">
										<input type="number" v-model="body.r" class="radius layui-input" />
									</div>
								</div>
								
							</div>
						</div>
					</div>
					<div class="controls">
						<i class="add layui-icon layui-icon-add-circle" @click="addBody"></i>
						<i class="reduce layui-icon layui-icon-reduce-circle" @click="reduceBody"></i>
					</div>
					<div class="btns">
						<button class="submit layui-btn layui-btn-normal" @click="closeSet">确定</button>
						<button class="random layui-btn layui-btn-primary layui-border-red" @click="random">随机</button>
					</div>
				</form>
			</section>
			<section class="view" v-show="!setting">
				<div id="set" @click="openSet">
					<i class="layui-icon layui-icon-set-fill"></i>
					<label>配置</label>
				</div>
				<input id="speed" type="range" min="0.1" max="5" step="0.1" value="1" @change="changeSpeed" />
			</section>
		</app>
		
<script>
document.addEventListener("plusready", function(){
	plus.navigator.setStatusBarBackground("#000");
	plus.navigator.setStatusBarStyle("light");
}, false);
</script>
<script>
class Body{
	constructor(pos, v, m, r, color){
		//this.pos = pos.clone();
		this.v = v.clone(); //速度 单位: m/s
		this.a = new THREE.Vector3(0, 0, 0); //加速度 单位: m/s²
		this.m = m;
		this.r = r;
		this.color = color;
		
		const geometry = new THREE.SphereGeometry(1, 36, 36); //几何体
		const material = new THREE.MeshLambertMaterial({ color }); //材质
		this.mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
		this.mesh.scale.set(r, r, r);
		this.mesh.position.copy(pos);
		this.pos = this.mesh.position; //绑定位置
		
		this.path = [this.pos.clone()];
		const pathGeometry = new THREE.BufferGeometry();
		pathGeometry.setFromPoints(this.path);
		const pathMaterial = new THREE.PointsMaterial({ color, sizeAttenuation: false, side: THREE.DoubleSide });
		this.pathMesh = new THREE.Points(pathGeometry, pathMaterial);
		
		this.lines = {
			a: new THREE.ArrowHelper(
				this.a.clone().normalize(),
				this.pos,
				this.a.length()*3+this.r,
				0xffffff
			),
			v: new THREE.ArrowHelper(
				this.v.clone().normalize(),
				this.pos,
				this.v.length()*1+this.r,
				color
			)
		};
	}
	
	//根据速度和加速度运动
	next(t){
		//单位: s
		this.pos
			.add( this.v.clone() .multiplyScalar(t) )
			.add( this.a.clone() .multiplyScalar(t*t/2) ); //x = v₀t + 1/2*a*t²
		this.v.add( this.a.clone().multiplyScalar(t) ); // v = v₀+at
		
		if (this.v.lengthSq() > 300*300){
			//alert([this.v.x, this.v.y, this.v.z, this.v.length(), this.v.lengthSq()])
			this.v.setLength(300);
		}
		
		this.lines.a.position.copy( this.pos );
		this.lines.v.position.copy( this.pos ); //更新位置
		this.lines.a.setLength( this.a.length()*3+this.r );
		this.lines.v.setLength( this.v.length()*1+this.r ); //更新长度
		this.lines.a.setDirection( this.a.clone().normalize() );
		this.lines.v.setDirection( this.v.clone().normalize() ); //更新方向
	}
}



class Bodies{
	constructor(bodies, G){
		this.bodies = bodies;
		this.G = G;
		this.speed = 1;
		this.id = null;
	}
	
	start(updateCallback=()=>{}){
		console.log("start")
		let t0 = +new Date();
		//let lastPath = +new Date();
		this.id = setInterval(()=>{
			const t = (+new Date() - t0) * this.speed;
			t0 = +new Date();
			
			if (t > 1000*this.speed) return;
			
			const integer = ~~t; //整数
			const decimal = t - integer; //小数
			const center = new THREE.Vector3(0, 0, 0);
			let num = 0;
			for (const body of this.bodies){
				center.add(body.pos.clone().multiplyScalar(body.m));
				num += body.m;
			}
			center.divideScalar(num);
				
			for (const [i, body] of Object.entries(this.bodies)){
				body.a.set(0, 0, 0);
				for (const [j, opp] of Object.entries(this.bodies)){
					if (i == j) continue;
					const delta = opp.pos.clone().sub(body.pos); //坐标差
					const r = delta.length(); //距离
					const a0 = this.G * opp.m * (r<20? -1/(r+1)**2: 1/r/r); //加速度标量
					
					body.a.add( delta.normalize().multiplyScalar(a0) ); //标准化向量，再乘以加速度，得到分加速度，加速度叠加
				}
				
				const delta = center.clone().sub(body.pos); //坐标差
				const r = delta.length(); //距离
				const a0 = 1; //加速度标量
				body.a.add( delta.normalize().multiplyScalar(a0) ); //标准化向量，再乘以加速度，得到分加速度，加速度叠加
				
				for (let t=0; t<integer; t++)
					body.next( 1e-3 );
				body.next(decimal / 1000);
			}
			
			//if (new Date() - lastPath > 30){
				for (const body of this.bodies){
					body.path.push( body.pos.clone() );
					body.pathMesh.geometry.setFromPoints( body.path );
				}
				//lastPath = +new Date();
			//}
			
			updateCallback();
		}, 0);
		return this;
	}
	
	stop(){
		console.log("stop")
		clearInterval(this.id);
		return this;
	}
	
	setSpeed(speed){
		this.speed = speed;
		return this;
	}
}


let bodySystem;
const app = Vue.createApp({
	data(){
		return {
			setting: true,
			landscape: window.innerWidth > window.innerHeight,
			scene: null,
			bodies: [{
				pos: new THREE.Vector3(
					Math.random(500, -500),
					Math.random(500, -500),
					Math.random(500, -500)
				),
				v: new THREE.Vector3(
					Math.random(20, -20),
					Math.random(20, -20),
					Math.random(20, -20)
				),
				m: 1,
				r: 10,
				color: "#ff0000"
			}, {
				pos: new THREE.Vector3(
					Math.random(500, -500),
					Math.random(500, -500),
					Math.random(500, -500)
				),
				v: new THREE.Vector3(
					Math.random(20, -20),
					Math.random(20, -20),
					Math.random(20, -20)
				),
				m: 1,
				r: 10,
				color: "#00ff00"
			}, {
				pos: new THREE.Vector3(
					Math.random(500, -500),
					Math.random(500, -500),
					Math.random(500, -500)
				),
				v: new THREE.Vector3(
					Math.random(20, -20),
					Math.random(20, -20),
					Math.random(20, -20)
				),
				m: 1,
				r: 10,
				color: "#0000ff"
			}]
		};
	},
	methods: {
		submit(){
			return false;
		},
		addBody(){
			const data = {
				pos: new THREE.Vector3(
					Math.random(500, -500),
					Math.random(500, -500),
					Math.random(500, -500)
				),
				v: new THREE.Vector3(
					Math.random(20, -20),
					Math.random(20, -20),
					Math.random(20, -20)
				),
				m: 1/27,
				r: 10/3,
				color: "#ffffff"
			};
			this.bodies.push(data);
			const body = new Body(data.pos, data.v, data.m, data.r, data.color);
			bodySystem.bodies.push(body);
			this.scene.add( body.mesh, body.pathMesh, body.lines.v, body.lines.a );
		},
		reduceBody(){
			
		},
		closeSet(){
			if (bodySystem)
				bodySystem.start(/*()=>{
					for (const [i, body] of Object.entries(bodySystem.bodies)){
						for (const [name, value] of Object.entries(body.pos))
							this.bodies[i].pos[name] = value;
						for (const [name, value] of Object.entries(body.v))
							this.bodies[i].v[name] = value;
					}
				}*/);
			this.setting = false;
		},
		random(){
			for (const body of this.bodies){
				for (const name of Object.keys(body.pos))
					body.pos[name] = Math.random(500, -500);
				for (const name of Object.keys(body.v))
					body.v[name] = Math.random(20, -20);
			}
		},
		openSet(){
			if (bodySystem)
				bodySystem.stop();
			
			for (const [i, body] of Object.entries(bodySystem.bodies)){
				for (const [name, value] of Object.entries(body.pos))
					this.bodies[i].pos[name] = value;
				for (const [name, value] of Object.entries(body.v))
					this.bodies[i].v[name] = value;
			}
			
			this.setting = true;
		},
		changeSpeed(){
			this.speed = +$("#speed").val();
			console.log("changeSpeed", this.speed)
			if (bodySystem)
				bodySystem.setSpeed(this.speed);
		}
	},
	mounted(){
		for (const [i, body] of Object.entries(this.bodies)){
			for (const name in body.pos)
				this.$watch(
					() => body.pos[name],
					(newVal, oldVal)=>{
						if (newVal === "" || isNaN(+newVal))
							body.pos[name] = oldVal;
						bodySystem.bodies[i].pos[name] = body.pos[name];
					}
				);
			for (const name in body.v)
				this.$watch(
					() => body.v[name],
					(newVal, oldVal)=>{
						if (newVal === "" || isNaN(+newVal))
							body.v[name] = oldVal;
						bodySystem.bodies[i].v[name] = body.v[name];
					}
				);
			this.$watch(
				() => body.m,
				(newVal, oldVal)=>{
					if (newVal === "" || isNaN(+newVal))
						body.m = oldVal;
					bodySystem.bodies[i].m = body.m;
				}
			);
			this.$watch(
				() => body.r,
				(newVal, oldVal)=>{
					if (newVal === "" || isNaN(+newVal))
						body.r = oldVal;
					bodySystem.bodies[i].r = body.r;
					bodySystem.bodies[i].mesh.scale.set(body.r, body.r, body.r);
				}
			);
		}
		
		
		/* 窗口大小 */
		let WIDTH = window.innerWidth, //窗口宽度
			HEIGHT = window.innerHeight; //窗口高度
		
		/* 场景 */
		const scene = new THREE.Scene();
		this.scene = scene;
		
		/* 坐标轴 */
		const axesHelper = new THREE.AxesHelper(666);
		scene.add(axesHelper);
		
		/* 光源 */
		//点光源
		const pointLight = new THREE.PointLight("#fff");
		pointLight.position.set(0, 0, 0); //点光源位置
		scene.add(pointLight); //点光源添加到场景中
		//环境光
		const ambientLight = new THREE.AmbientLight("#888");
		scene.add(ambientLight);
		
		/* 相机 */
		const camera = new THREE.PerspectiveCamera(45, WIDTH/HEIGHT, 1, 666666*100*1000);
		camera.position.set(666, 666, 666); //设置相机位置
		camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
		
		/* 渲染器 */
		const renderer = new THREE.WebGLRenderer({
			antialias: true //抗锯齿
		});
		renderer.setSize(WIDTH, HEIGHT);//设置渲染区域尺寸
		renderer.setClearColor(0x000000, 1); //设置背景颜色
		renderer.domElement.style.margin = "0";
		$("app > .view").append(renderer.domElement); //body元素中插入canvas对象
		
		//控制
		const control = new THREE.OrbitControls(camera, renderer.domElement);
		
		//stats
		const stats = new Stats();
		stats.showPanel(0);
		$("app > .view").append(stats.dom);
		
		//渲染循环
		(function render(){
			requestAnimationFrame(render);
			renderer.render(scene, camera);
			stats.update();
		})();
		
		//窗口大小调整
		window.onresize = function(){
			WIDTH = window.innerWidth,
			HEIGHT = window.innerHeight;
			renderer.setSize(window.innerWidth, window.innerHeight);
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix(); //更新投影矩阵
		};
		
		bodySystem = new Bodies(
			this.bodies.map( body => new Body(body.pos, body.v, body.m, body.r, body.color) ),
			1e6
		);
		console.log(bodySystem)
		scene.add(...bodySystem.bodies.map(body => body.mesh));
		scene.add(...bodySystem.bodies.map(body => body.pathMesh));
		scene.add(...bodySystem.bodies.map(body => body.lines.v));
		scene.add(...bodySystem.bodies.map(body => body.lines.a));
	}
}).mount("app");
</script>
		
	</body>
</html>